package safe

import (
	"context"
	"fmt"
	"runtime/debug"

	"gitee.com/zacyuan/yuan/pkg/log"
)

// Go executes the given function in a new goroutine.
// If the given function returns without panicking,
// any panics that occur in the goroutine will be caught and recovered.
func Go(g func()) {
	go func() {
		defer Recover(nil)
		if g != nil {
			g()
		}
	}()
}

// Call executes the given function in the current goroutine.
// If the given function returns without panicking,
// any panics that occur in the goroutine will be caught and recovered.
func Call(c func()) {
	defer Recover(nil)
	if c != nil {
		c()
	}
}

// Callerr executes the given function in the current goroutine.
// If the given function returns without panicking, any panics that
// occur in the goroutine will be caught and recovered.
// If the given function panics or returns an error, Callerr will
// return that error wrapped in a fmt.Errorf.
func Callerr(c func() error) (err error) {
	defer Recover(
		func(e any) {
			err = fmt.Errorf("%v", e)
		})
	if c != nil {
		return c()
	}
	return nil
}

// Callctx executes the given function in the current goroutine.
// If the context has a deadline, the function will be run in a
// new goroutine and Callctx will return an error if the
// context's Done channel is closed before the function completes.
// If the given function returns without panicking, any panics that
// occur in the goroutine will be caught and recovered.
func Callctx(ctx context.Context, c func()) error {
	if _, ok := ctx.Deadline(); !ok {
		Call(c)
		return nil
	}
	done := make(chan struct{}, 1)
	Go(func() {
		c()
		done <- struct{}{}
	})
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-done:
		return nil
	}
}

// Recover is a deferred function that can be used to catch and recover
// from panics. If a panic occurs and is recovered, any error
// returned by the given hook function will be logged and the function
// will return.
func Recover(hook func(any)) {
	if err := recover(); err != nil {
		log.Error("Panic: %v", err)
		log.Error("Stack:\n %s", debug.Stack())

		if hook != nil {
			hook(err)
		}
	}
}
